package com.yjsn.base.utils;


import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.yjsn.base.config.RedisCache;
import com.yjsn.base.exception.SignException;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.*;

public class SignUtil
{
    private static final Logger logger = LoggerFactory.getLogger(SignUtil.class);

    public static char[] hexChar =
            { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
                    'e', 'f' };
    public static String hashType = "MD5";
    public static String encrypt(String data, String key)
            throws Exception
    {
        byte[] bytesKey = Base64.getDecoder().decode(key);

        SecretKeySpec secretKeySpec = new SecretKeySpec(bytesKey, "AES");

        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(1, secretKeySpec);
        byte[] bytes = cipher.doFinal(data.getBytes("utf-8"));
        return parseByte2HexStr(bytes);
    }

    public static String decrypt(String data, String key)
            throws Exception
    {
        byte[] bytesKey = Base64.getDecoder().decode(key);

        SecretKeySpec secretKeySpec = new SecretKeySpec(bytesKey, "AES");

        byte[] encryptBytes = parseHexStr2Byte(data);

        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(2, secretKeySpec);

        byte[] decryptBytes = cipher.doFinal(encryptBytes);
        return new String(decryptBytes, "UTF-8");
    }

    public static String MD5Encrypt(String str)
            throws Exception
    {
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        byte[] digest = md5.digest(str.getBytes("utf-8"));
        String newstr = Base64.getEncoder().encodeToString(digest);
        return newstr;
    }

    public static String parseByte2HexStr(byte[] buf)
    {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < buf.length; i++)
        {
            String hex = Integer.toHexString(buf[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            sb.append(hex.toUpperCase());
        }
        return sb.toString();
    }

    public static byte[] parseHexStr2Byte(String hexStr)
    {
        if (hexStr.length() < 1) {
            return null;
        }
        byte[] result = new byte[hexStr.length() / 2];
        for (int i = 0; i < hexStr.length() / 2; i++)
        {
            int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
            int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
            result[i] = ((byte)(high * 16 + low));
        }
        return result;
    }

    /**
     * 对输入的Map进行排序，得到一个新的有序的Map。
     * 将有序Map中的键值对拼接成字符串。
     * 将拼接好的字符串加上"&key=" + key进行连接。
     * 使用MD5算法对连接后的字符串进行加密。
     * 返回加密后的字符串作为签名
     * @param map 参数集合
     * @param key  加密密钥
     * @return 加密后的字符串
     */
    public static String generateSign(Map<String, Object> map, String key) {
        Map<String, Object> orderMap = order(map);
        String join = mapJoin(orderMap);
        try {
            return getMD5String(join + "&key=" + key);
        } catch (Exception e) {
            logger.error("generateSign加密失败");
            return null;
        }
    }

    /**
     * 对map进行排序
     * 该方法主要用于对map中的key进行排序，以确保后续处理的一致性和可预测性
     * 由于普通的HashMap是无序的，而LinkedHashMap可以按照插入顺序或者访问顺序来保持条目的顺序
     * 这里选择使用LinkedHashMap来保持排序后的结果
     * @param map 输入的Map对象，包含了无序的键值对
     * @return 返回排序后的Map对象，其中不包含"sign"键
     */
    public static Map<String, Object> order(Map<String, Object> map) {
        HashMap<String, Object> tempMap = new LinkedHashMap<>();
        List<Map.Entry<String, Object>> infoIds = new ArrayList<>(map.entrySet());
        infoIds.sort(Comparator.comparing(Map.Entry::getKey));
        for (int i = 0; i < infoIds.size(); i++) {
            Map.Entry<String, Object> item = infoIds.get(i);
            if (!"sign".equals(item.getKey())) {
                tempMap.put(item.getKey(), item.getValue());
            }
        }
        return tempMap;
    }

    /**
     * 将Map对象中的键值对转换为字符串，格式为"key1=value1&key2=value2&...&keyN=valueN"。
     * 仅包含非空值的键值对。
     * @param map 待转换的Map对象，键和值可以是任意类型。
     * @return 转换后的字符串，如果Map为空或所有值为空，则返回空字符串。
     */
    public static String mapJoin(Map<String, Object> map) {
        StringBuilder stringBuilder = new StringBuilder();
        for (String key : map.keySet()) {
            if (map.get(key) != null && !"".equals(map.get(key))) {
                stringBuilder.append(key).append("=").append(map.get(key)).append("&");
            }
        }
        if (stringBuilder.length() > 0) {
            stringBuilder.deleteCharAt(stringBuilder.length() - 1);
        }
        return stringBuilder.toString();
    }

    /**
     * 将输入字符串加密为MD5散列值
     * @param input 待加密的字符串
     * @return 加密后的MD5散列值字符串
     * @throws Exception 如果无法获取MD5算法实例或其他加密错误发生
     */
    public static String getMD5String(String input) throws Exception
    {
        MessageDigest md5 = MessageDigest.getInstance(hashType);
        logger.debug("MD5加密前：{}", input);
        md5.update(input.getBytes());
        return toHexString(md5.digest());
    }

    /**
     * 将字节数组转换为十六进制字符串
     * @param b 字节数组
     * @return
     */
    private static String toHexString(byte[] b)
    {
        StringBuffer sb = new StringBuffer(b.length * 2);
        for (int i = 0; i < b.length; i++)
        {
            sb.append(hexChar[(b[i] & 0xf0) >>> 4]);
            sb.append(hexChar[b[i] & 0x0f]);
        }
        return sb.toString();
    }

    /**
     * 获取时间戳
     * @param request
     * @return
     */
    public static Long getDateTimestamp( HttpServletRequest request) {
        String timestampStr= request.getHeader("timestamp");
        if (StringUtils.isBlank(timestampStr)) {
            logger.error("######timestamp is null");
            throw new SignException("缺少必要参数");
        }
        long timestamp = Long.parseLong(timestampStr);
        long currentTimeMillis = System.currentTimeMillis();
        //有效时长为1分钟
        if (currentTimeMillis - timestamp > 1000 * 60 ) {
            logger.error("#####timestamp:{},currentTime:{}",timestamp,currentTimeMillis);
            throw new SignException("非法请求");
        }
        return timestamp;
    }

    /**
     * 获取随机数
     * @param request
     * @return
     */
    public static String getRandom(HttpServletRequest request) {
        String random= request.getHeader("random");
        if (StringUtils.isBlank(random)) {
            logger.error("######random is null");
            throw new SignException("缺少必要参数");
        }
        return random;
    }

    /**
     * 获取来源
     * @param request
     * @return
     */
    public static String getSource(HttpServletRequest request) {
        String source= request.getHeader("source");
        if (StringUtils.isBlank(source)) {
            logger.error("######source is null");
            throw new SignException("缺少必要参数");
        }
        return source;
    }

    /**
     * 获取签名
     * @param request
     * @return
     */
    public static String getSignature(HttpServletRequest request, RedisCache redisCache, int expireTime) {
        String sign = request.getHeader("sign");
        if (StringUtils.isBlank(sign)) {
            logger.error("######sign is null");
            throw new SignException("缺少必要参数");
        }
        //如果requestId存在redis中直接返回
        String temp = redisCache.getCacheObject(sign);
        if (StringUtils.isNotBlank(temp)) {
            logger.error("sign is validate: {}",temp);
            throw new SignException("非法请求");
        }
        redisCache.setex(sign, expireTime ,sign);
        return sign;
    }

    /**
     * 验证签名
     * @param params
     * @param sign
     * @param timestamp
     * @param random
     * @param source
     * @return
     */
    public static boolean checkSign(JSONObject params, String sign, Long timestamp, String random, String source, String signKey){
        Map<String, Object> paramsMap = JSON.toJavaObject(params,Map.class);
        if(Objects.isNull(paramsMap)){
            paramsMap = new HashMap<>();
        }
        paramsMap.put("timestamp",timestamp);
        paramsMap.put("random",random);
        paramsMap.put("source",source);
        String checkSign = SignUtil.generateSign(paramsMap, signKey);
        logger.info("SignUtil.checkSign checkSign is : {},sign is : {}",checkSign,sign);
        if(checkSign.equals(sign)) return true;
        return false;
    }

}
